package com.basker.pisces.core.meta.bean;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.core.ResolvableType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.basker.pisces.core.DataContractGenerator;
import com.basker.pisces.core.IDataContract;
import com.basker.pisces.core.enums.DataObjectState;
import com.basker.pisces.core.meta.AbstractDataObjectMeta;
import com.basker.pisces.core.meta.DataObjectField;
import com.basker.pisces.core.meta.IDataField;
import com.basker.pisces.core.meta.IDataObjectField;
import com.basker.pisces.core.meta.IDataObjectMeta;
import com.basker.pisces.core.meta.IReferenceField;
import com.basker.pisces.core.meta.annotation.CodeField;
import com.basker.pisces.core.meta.annotation.CollectionField;
import com.basker.pisces.core.meta.annotation.DataField;
import com.basker.pisces.core.meta.annotation.DataObjectMeta;
import com.basker.pisces.core.meta.annotation.NameField;
import com.basker.pisces.core.meta.annotation.ObjectField;
import com.basker.pisces.core.meta.annotation.ReferenceField;
import com.basker.pisces.core.meta.annotation.TimestampField;
import com.basker.pisces.core.meta.utils.DataObjectMetaUtils;
import com.basker.pisces.spring.SpringApplicationContext;
import com.basker.pisces.utils.ReflectionUtils;
import com.esotericsoftware.reflectasm.ConstructorAccess;

/**
 * 通过反射的方式生成的元信息，该实例不可直接创建，出于性能考虑需要缓存，可以通过{@link BeanMetaManager}中定义的方法获取实例
 *
 * <li>其对应的bean无需定义各种系统内置字段，框架会通过动态代理的方式创建，具体参见{@link DataContractGenerator#create()}
 * <li>bean需要通过各类注解定义一些信息
 *
 * @author hangwen
 */
public class BeanMeta extends AbstractDataObjectMeta {

    private static final boolean USEASM = SpringApplicationContext.getConfig().getBoolean("pisces.asm.enable", true);

    private Class<?> beanClass;
    private ConstructorAccess<?> beanConstructorAccess;

    private volatile boolean generateDataContractClass = true;
    private volatile Class<?> dataContractClass;
    private ConstructorAccess<?> dataContractConstructorAccess;

    private Map<String, BeanFieldDescriptor> beanFieldDescriptors = new TreeMap<>();
    private Map<String, RefFieldWrapper> refFields = new HashMap<>(16);

    BeanMeta(Class<?> beanClass) {
        this.setBeanClass(beanClass);
    }

    @Override
    public Object createDataObject() {
        if (this.generateDataContractClass && DataContractGenerator.isEnable()) {
            Class<?> dataContractClass = this.getDataContractClass();

            Object bean = USEASM ? this.dataContractConstructorAccess.newInstance()
                    : ReflectionUtils.createNewInstance(dataContractClass);
            ((IDataContract) bean).setReadOnly(this.isReadOnly());

            return bean;
        } else {
            return USEASM ? this.beanConstructorAccess.newInstance() : ReflectionUtils.createNewInstance(beanClass);
        }
    }

    @Override
    public Class<?> getDataObjectClass() {
        return this.beanClass;
    }

    @Override
    public Object getOldValue(Object dataObject, IDataField dataField) {
        if (dataObject instanceof IDataContract) {
            return ((IDataContract) dataObject).getOldValue(dataField.getName());
        }

        return null;
    }

    @Override
    public DataObjectState getState(Object dataObject) {
        if (dataObject instanceof IDataContract) {
            return ((IDataContract) dataObject).getState();
        }

        return DataObjectState.NoDefinition;
    }

    @Override
    public boolean isChanged(Object dataObject, IDataField dataField) {
        if (dataObject instanceof IDataContract) {
            return ((IDataContract) dataObject).isChanged(dataField.getName());
        }

        return true;
    }

    @Override
    public boolean isFromDB(Object dataObject) {
        if (dataObject instanceof IDataContract) {
            return ((IDataContract) dataObject).isFromDB();
        }

        return false;
    }

    @Override
    public boolean isFromDB(Object dataObject, String subTableName) {
        if (dataObject instanceof IDataContract) {
            return ((IDataContract) dataObject).isFromDB(subTableName);
        }

        return false;
    }

    public boolean isGenerateDataContractClass() {
        return generateDataContractClass;
    }

    @Override
    public void setFromDB(Object dataObject) {
        if (dataObject instanceof IDataContract) {
            ((IDataContract) dataObject).setFromDB(true);
        }
    }

    @Override
    public void setFromDB(Object dataObject, String subTableName) {
        if (dataObject instanceof IDataContract) {
            ((IDataContract) dataObject).setFromDB(subTableName, true);
        }
    }

    public void setGenerateDataContractClass(boolean generateDataContractClass) {
        this.generateDataContractClass = generateDataContractClass;
    }

    @Override
    public void setState(Object dataObject, DataObjectState state) {
        if (dataObject instanceof IDataContract) {
            ((IDataContract) dataObject).setState(state);

            if (state.equals(DataObjectState.UnChanged)) {
                ((IDataContract) dataObject).clearOldValues();
            }
        }
    }

    protected IDataObjectMeta createCollectionMeta(String fieldName, Class<?> itemClass) {
        return BeanMetaManager.getBeanMeta(itemClass);
    }

    protected IDataObjectMeta createReferenceMeta(String fieldName, Class<?> refClazz, String[] refFields) {
        return BeanMetaManager.getReferenceBeanMeta(refClazz, refFields);
    }

    protected Set<String> getRefFieldSet(IDataObjectMeta refMeta, String[] otherRefFields) {
        Set<String> refFields = new HashSet<>();

        refFields.addAll(DataObjectMetaUtils.getIdCodeNameFields(refMeta));

        if (!ArrayUtils.isEmpty(otherRefFields)) {
            refFields.addAll(Arrays.asList(otherRefFields));
        }

        return refFields;
    }

    protected void loadFromAnnatation(DataObjectMeta aMeta) {
        String name = aMeta.name();
        if (StringUtils.isEmpty(name)) {
            name = this.beanClass.getName();
        }

        this.setName(name);

        this.setDisplayName(aMeta.displayName());
        this.setTableName(aMeta.tableName());
        this.setSubTableNames(aMeta.subTableNames());
        this.setSchema(aMeta.schema());

        this.setReadOnly(aMeta.isReadOnly());
        this.setGenerateDataContractClass(aMeta.generateDataContractClass());
    }

    protected void prepareBeanFieldDescriptors(Class<?> beanClass) {
        int refFieldCount = 0;

        BeanFieldDescriptor[] beanFieldDescriptors = BeanFieldDescriptor.getBeanFieldDescriptors(beanClass);
        for (BeanFieldDescriptor beanFieldDescriptor : beanFieldDescriptors) {
            String name = beanFieldDescriptor.getName();

            if (beanFieldDescriptor.isFieldAnnotationPresent() && this.validField(name)) {
                this.beanFieldDescriptors.put(name, beanFieldDescriptor);

                Annotation annotation = beanFieldDescriptor.getFieldAnnotation();
                if (annotation instanceof ReferenceField) {
                    refFieldCount++;
                }
            }
        }

        if (refFieldCount > 0) {
            this.refFields = new HashMap<>(refFieldCount, 1);
        }
    }

    protected void setBeanClass(Class<?> beanClass) {
        this.beanClass = beanClass;
        this.beanConstructorAccess = ConstructorAccess.get(beanClass);

        DataObjectMeta beanMeta = this.beanClass.getAnnotation(DataObjectMeta.class);

        if (beanMeta != null) {
            this.loadFromAnnatation(beanMeta);
        } else {
            // this.setName(this.beanClass.getName());
            throw new RuntimeException("class '" + beanClass.getName() + "' has no 'DataObjectMeta' annotation!");
        }
    }

    protected boolean validField(String name) {
        return true;
    }

    void build() {
        this.prepareBeanFieldDescriptors(beanClass);
        this.loadFields(this.beanFieldDescriptors.values());
        this.valid();

    }

    private Class<?> getDataContractClass() {
        if (this.dataContractClass == null) {
            synchronized (this) {
                if (this.dataContractClass == null) {
                    this.dataContractClass = new DataContractGenerator(beanClass).getDataContractClass();
                    this.dataContractConstructorAccess = ConstructorAccess.get(this.dataContractClass);
                }
            }
        }

        return this.dataContractClass;
    }

    private IDataObjectMeta lazyLoadRefMeta(String refFieldName) {
        RefFieldWrapper wrapper = refFields.get(refFieldName);
        if (wrapper == null) {
            return null;
        }

        Class<?> refClazz = wrapper.refClazz;
        String[] refFields = wrapper.refFields;

        IDataObjectMeta fullMeta = BeanMetaManager.getBeanMeta(refClazz);
        if (refFields.length == 1 && refFields[0].equalsIgnoreCase(ReferenceField.FULL)) {
            return fullMeta;
        }

        Set<String> refFieldSet = this.getRefFieldSet(fullMeta, refFields);

        IDataObjectMeta referenceMeta = this.createReferenceMeta(refFieldName, refClazz,
                refFieldSet.toArray(new String[refFieldSet.size()]));

        return referenceMeta;
    }

    private void lazyLoadRefMetas() {
        if (CollectionUtils.isEmpty(this.refFields)) {
            return;
        }

        for (IReferenceField refField : this.getReferenceFields()) {
            IDataObjectMeta meta = this.lazyLoadRefMeta(refField.getName());
            if (meta != null) {
                ((DataObjectField) refField).setObjectMeta(meta);
            }
        }
    }

    private void loadCollectionField(BeanFieldDescriptor beanFieldDescriptor,
                                     CollectionField collectionFieldAnnotation) {
        BeanCollectionField collectionField = BeanFieldFactory.createCollectionField(beanFieldDescriptor,
                collectionFieldAnnotation);

        String name = beanFieldDescriptor.getName();
        IDataObjectMeta itemObjectMeta = null;

        Class<?>[] itemClass = collectionFieldAnnotation.itemClass();
        if (itemClass.length > 0) {
            itemObjectMeta = this.createCollectionMeta(name, itemClass[0]);
        } else {
            ResolvableType methodReturnType = ResolvableType.forMethodReturnType(beanFieldDescriptor.getGetMethod());
            Class<?> genericClass = methodReturnType.getGeneric(0).resolve();

            if (genericClass != null && !genericClass.equals(Object.class)) {
                itemObjectMeta = this.createCollectionMeta(name, genericClass);
            }
        }

        collectionField.setItemObjectMeta(itemObjectMeta);

        this.addField(collectionField);
    }

    private void loadDataField(BeanFieldDescriptor beanFieldDescriptor, DataField dataFieldAnnotation) {
        IDataField dataField = BeanFieldFactory.createDataField(beanFieldDescriptor);

        if (beanFieldDescriptor.isAnnotationPresent(CodeField.class)) {
            this.setCodeField(dataField);
        }

        if (beanFieldDescriptor.isAnnotationPresent(NameField.class)) {
            this.setNameField(dataField);
        }

        if (beanFieldDescriptor.isAnnotationPresent(TimestampField.class)) {
            this.setTimestampField(dataField);
            this.setTimestampType(beanFieldDescriptor.getAnnotation(TimestampField.class).type());
        }

        this.addField(dataField);
    }

    private void loadFields(Collection<BeanFieldDescriptor> beanFieldDescriptors) {
        for (BeanFieldDescriptor beanFieldDescriptor : beanFieldDescriptors) {
            Annotation annotation = beanFieldDescriptor.getFieldAnnotation();

            if (annotation instanceof DataField) {
                this.loadDataField(beanFieldDescriptor, (DataField) annotation);
                continue;
            }

            if (annotation instanceof ObjectField) {
                this.loadObjectField(beanFieldDescriptor, (ObjectField) annotation);
                continue;
            }

            if (annotation instanceof ReferenceField) {
                this.loadReferenceField(beanFieldDescriptor, (ReferenceField) annotation);
                continue;
            }

            if (annotation instanceof CollectionField) {
                this.loadCollectionField(beanFieldDescriptor, (CollectionField) annotation);
                continue;
            }
        }

        this.lazyLoadRefMetas();
    }

    private void loadObjectField(BeanFieldDescriptor beanFieldDescriptor, ObjectField objectFieldAnnotation) {
        IDataObjectField objectField = BeanFieldFactory.createDataObjectField(beanFieldDescriptor,
                objectFieldAnnotation);
        this.addField(objectField);
    }

    private void loadReferenceField(BeanFieldDescriptor beanFieldDescriptor, ReferenceField referenceFieldAnnotation) {
        BeanReferenceField refField = BeanFieldFactory.createReferenceField(beanFieldDescriptor,
                referenceFieldAnnotation);

        this.addField(refField);

        // 暂存关联字段和其关联类型
        String name = beanFieldDescriptor.getName();
        Class<?> refClazz = beanFieldDescriptor.getProperyType();
        RefFieldWrapper wrapper = new RefFieldWrapper(refClazz, referenceFieldAnnotation.refFields());

        refFields.put(name, wrapper);

    }

    private class RefFieldWrapper {
        Class<?> refClazz;

        String[] refFields;

        public RefFieldWrapper(Class<?> refClazz, String[] refFields) {
            this.refClazz = refClazz;
            this.refFields = refFields;
        }
    }

}
